@jsenv/snapshot 2.8.3 → 2.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/snapshot",
3
- "version": "2.8.3",
3
+ "version": "2.8.5",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -35,9 +35,9 @@
35
35
  "dependencies": {
36
36
  "@jsenv/assert": "4.1.15",
37
37
  "@jsenv/ast": "6.2.14",
38
- "@jsenv/exception": "1.0.1",
39
- "@jsenv/filesystem": "4.9.9",
40
- "@jsenv/terminal-recorder": "1.4.3",
38
+ "@jsenv/exception": "1.0.2",
39
+ "@jsenv/filesystem": "4.9.10",
40
+ "@jsenv/terminal-recorder": "1.4.4",
41
41
  "@jsenv/urls": "2.5.2",
42
42
  "@jsenv/utils": "2.1.2",
43
43
  "ansi-regex": "6.0.1",
@@ -180,7 +180,7 @@ export const takeDirectorySnapshot = (
180
180
  return URL_META.urlChildMayMatch({
181
181
  url,
182
182
  associations,
183
- predicate: (meta) => Boolean(meta.action),
183
+ predicate: (meta) => meta.action && meta.action !== "ignore",
184
184
  });
185
185
  };
186
186
  const shouldIncludeFile = (url) => {
@@ -188,9 +188,13 @@ export const takeDirectorySnapshot = (
188
188
  url,
189
189
  associations,
190
190
  });
191
- return meta.action === true || meta.action === "presence_only";
191
+ return (
192
+ meta.action === true ||
193
+ meta.action === "compare" ||
194
+ meta.action === "compare_presence_only"
195
+ );
192
196
  };
193
- const shouldCompareFile = (url) => {
197
+ const shouldCompareFileContent = (url) => {
194
198
  const meta = URL_META.applyAssociations({
195
199
  url,
196
200
  associations,
@@ -200,7 +204,7 @@ export const takeDirectorySnapshot = (
200
204
  const directorySnapshot = createDirectorySnapshot(directoryUrl, {
201
205
  shouldVisitDirectory,
202
206
  shouldIncludeFile,
203
- shouldCompareFile,
207
+ shouldCompareFileContent,
204
208
  clean: true,
205
209
  });
206
210
  return {
@@ -209,7 +213,7 @@ export const takeDirectorySnapshot = (
209
213
  const nextDirectorySnapshot = createDirectorySnapshot(directoryUrl, {
210
214
  shouldVisitDirectory,
211
215
  shouldIncludeFile,
212
- shouldCompareFile,
216
+ shouldCompareFileContent,
213
217
  });
214
218
  directorySnapshot.compare(nextDirectorySnapshot, { throwWhenDiff });
215
219
  },
@@ -237,7 +241,7 @@ export const takeDirectorySnapshot = (
237
241
  };
238
242
  const createDirectorySnapshot = (
239
243
  directoryUrl,
240
- { shouldVisitDirectory, shouldIncludeFile, shouldCompareFile, clean },
244
+ { shouldVisitDirectory, shouldIncludeFile, shouldCompareFileContent, clean },
241
245
  ) => {
242
246
  const directorySnapshot = {
243
247
  type: "directory",
@@ -322,7 +326,7 @@ ${extraUrls.join("\n")}`);
322
326
  // content
323
327
  {
324
328
  for (const relativeUrl of relativeUrls) {
325
- if (!shouldCompareFile(new URL(relativeUrl, directoryUrl))) {
329
+ if (!shouldCompareFileContent(new URL(relativeUrl, directoryUrl))) {
326
330
  continue;
327
331
  }
328
332
  const snapshot = directoryContentSnapshot[relativeUrl];
@@ -384,7 +388,7 @@ ${extraUrls.join("\n")}`);
384
388
  {
385
389
  shouldVisitDirectory,
386
390
  shouldIncludeFile,
387
- shouldCompareFile,
391
+ shouldCompareFileContent,
388
392
  clean,
389
393
  },
390
394
  );
@@ -4,13 +4,15 @@ import { filesystemSideEffects } from "./filesystem/filesystem_side_effects.js";
4
4
  import { logSideEffects } from "./log/log_side_effects.js";
5
5
 
6
6
  export const createCaptureSideEffects = ({
7
+ sourceFileUrl,
7
8
  logEffects = true,
8
9
  filesystemEffects = true,
10
+ filesystemActions,
9
11
  rootDirectoryUrl,
10
12
  replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
11
13
  rootDirectoryUrl,
12
14
  }),
13
- } = {}) => {
15
+ }) => {
14
16
  const detectors = [];
15
17
  if (logEffects) {
16
18
  detectors.push(logSideEffects(logEffects === true ? {} : logEffects));
@@ -20,7 +22,9 @@ export const createCaptureSideEffects = ({
20
22
  filesystemSideEffectsDetector = filesystemSideEffects(
21
23
  filesystemEffects === true ? {} : filesystemEffects,
22
24
  {
25
+ sourceFileUrl,
23
26
  replaceFilesystemWellKnownValues,
27
+ filesystemActions,
24
28
  },
25
29
  );
26
30
  detectors.push(filesystemSideEffectsDetector);
@@ -7,17 +7,16 @@ import { groupFileSideEffectsPerDirectory } from "./group_file_side_effects_per_
7
7
  import { spyFilesystemCalls } from "./spy_filesystem_calls.js";
8
8
 
9
9
  const filesystemSideEffectsOptionsDefault = {
10
- include: null,
11
10
  preserve: false,
12
11
  baseDirectory: "",
13
- textualFilesIntoDirectory: false,
12
+ textualFilesInline: false,
14
13
  };
15
14
  const INLINE_MAX_LINES = 20;
16
15
  const INLINE_MAX_LENGTH = 2000;
17
16
 
18
17
  export const filesystemSideEffects = (
19
18
  filesystemSideEffectsOptions,
20
- { replaceFilesystemWellKnownValues },
19
+ { sourceFileUrl, filesystemActions, replaceFilesystemWellKnownValues },
21
20
  ) => {
22
21
  filesystemSideEffectsOptions = {
23
22
  ...filesystemSideEffectsOptionsDefault,
@@ -42,10 +41,11 @@ export const filesystemSideEffects = (
42
41
  name: "filesystem",
43
42
  setBaseDirectory,
44
43
  install: (addSideEffect, { addSkippableHandler, addFinallyCallback }) => {
45
- let { include, preserve, textualFilesIntoDirectory } =
46
- filesystemSideEffectsOptions;
44
+ let { preserve, textualFilesInline } = filesystemSideEffectsOptions;
47
45
  if (filesystemSideEffectsOptions.baseDirectory) {
48
46
  setBaseDirectory(filesystemSideEffectsOptions.baseDirectory);
47
+ } else if (sourceFileUrl) {
48
+ setBaseDirectory(new URL("./", sourceFileUrl));
49
49
  }
50
50
  const getUrlRelativeToBase = (url) => {
51
51
  if (baseDirectory) {
@@ -155,7 +155,7 @@ export const filesystemSideEffects = (
155
155
  if (outDirectoryReason) {
156
156
  const outUrlRelativeToCommonDirectory = urlToRelativeUrl(
157
157
  text.urlInsideOutDirectory,
158
- options.sideEffectFileUrl,
158
+ options.sideEffectMdFileUrl,
159
159
  );
160
160
  groupMd += `${"#".repeat(2)} ${urlRelativeToCommonDirectory}
161
161
  ${renderFileContent(
@@ -193,7 +193,7 @@ ${renderFileContent(
193
193
  );
194
194
  const commonDirectoryOutRelativeUrl = urlToRelativeUrl(
195
195
  commonDirectoryOutUrl,
196
- options.sideEffectFileUrl,
196
+ options.sideEffectMdFileUrl,
197
197
  { preferRelativeNotation: true },
198
198
  );
199
199
  return {
@@ -219,12 +219,14 @@ ${renderFileContent(
219
219
  const isTextual = CONTENT_TYPE.isTextual(contentType);
220
220
  let outDirectoryReason;
221
221
  if (isTextual) {
222
- if (textualFilesIntoDirectory) {
223
- outDirectoryReason = "textual_in_directory_option";
224
- } else if (String(buffer).split("\n").length > INLINE_MAX_LINES) {
225
- outDirectoryReason = "lot_of_lines";
226
- } else if (buffer.size > INLINE_MAX_LENGTH) {
227
- outDirectoryReason = "lot_of_chars";
222
+ if (textualFilesInline) {
223
+ if (String(buffer).split("\n").length > INLINE_MAX_LINES) {
224
+ outDirectoryReason = "lot_of_lines";
225
+ } else if (buffer.size > INLINE_MAX_LENGTH) {
226
+ outDirectoryReason = "lot_of_chars";
227
+ }
228
+ } else {
229
+ outDirectoryReason = "text";
228
230
  }
229
231
  } else {
230
232
  outDirectoryReason = "binary";
@@ -240,7 +242,7 @@ ${renderFileContent(
240
242
  outDirectoryReason,
241
243
  },
242
244
  render: {
243
- md: ({ sideEffectFileUrl, generateOutFileUrl }) => {
245
+ md: ({ sideEffectMdFileUrl, generateOutFileUrl }) => {
244
246
  const urlRelativeToBase = getUrlRelativeToBase(url);
245
247
  if (outDirectoryReason) {
246
248
  let urlInsideOutDirectory = getUrlInsideOutDirectory(
@@ -267,7 +269,7 @@ ${renderFileContent(
267
269
  }
268
270
  const outRelativeUrl = urlToRelativeUrl(
269
271
  urlInsideOutDirectory,
270
- sideEffectFileUrl,
272
+ sideEffectMdFileUrl,
271
273
  {
272
274
  preferRelativeNotation: true,
273
275
  },
@@ -311,7 +313,7 @@ ${renderFileContent(
311
313
  },
312
314
  },
313
315
  {
314
- include,
316
+ include: filesystemActions,
315
317
  undoFilesystemSideEffects: !preserve,
316
318
  },
317
319
  );
@@ -9,7 +9,7 @@ import {
9
9
  writeFileSync,
10
10
  } from "@jsenv/filesystem";
11
11
  import { URL_META } from "@jsenv/url-meta";
12
- import { yieldAncestorUrls } from "@jsenv/urls";
12
+ import { ensurePathnameTrailingSlash, yieldAncestorUrls } from "@jsenv/urls";
13
13
  import { readFileSync, statSync } from "node:fs";
14
14
  import { pathToFileURL } from "node:url";
15
15
  import {
@@ -28,9 +28,20 @@ export const spyFilesystemCalls = (
28
28
  },
29
29
  { include, undoFilesystemSideEffects } = {},
30
30
  ) => {
31
- const shouldReport = include
32
- ? URL_META.createFilter(include, "file:///")
33
- : () => true;
31
+ const getAction = include
32
+ ? (() => {
33
+ const associations = URL_META.resolveAssociations(
34
+ {
35
+ action: include,
36
+ },
37
+ "file:///",
38
+ );
39
+ return (url) => {
40
+ const meta = URL_META.applyAssociations({ url, associations });
41
+ return meta.action;
42
+ };
43
+ })()
44
+ : () => "compare";
34
45
 
35
46
  const _internalFs = process.binding("fs");
36
47
  const filesystemStateInfoMap = new Map();
@@ -58,35 +69,49 @@ export const spyFilesystemCalls = (
58
69
  // function did not have any effect on the file
59
70
  return;
60
71
  }
61
- if (!shouldReport(fileUrl)) {
62
- return;
63
- }
64
- if (undoFilesystemSideEffects && !fileRestoreMap.has(fileUrl)) {
65
- if (stateBefore.found) {
66
- fileRestoreMap.set(fileUrl, () => {
67
- writeFileSync(fileUrl, stateBefore.buffer);
68
- });
69
- } else {
70
- fileRestoreMap.set(fileUrl, () => {
71
- removeFileSync(fileUrl, { allowUseless: true });
72
- });
72
+ const action = getAction(fileUrl);
73
+ const shouldCompare =
74
+ action === "compare" ||
75
+ action === "compare_presence_only" ||
76
+ action === true;
77
+ if (action === "undo" || shouldCompare) {
78
+ if (undoFilesystemSideEffects && !fileRestoreMap.has(fileUrl)) {
79
+ if (stateBefore.found) {
80
+ fileRestoreMap.set(fileUrl, () => {
81
+ writeFileSync(fileUrl, stateBefore.buffer);
82
+ });
83
+ } else {
84
+ fileRestoreMap.set(fileUrl, () => {
85
+ removeFileSync(fileUrl, { allowUseless: true });
86
+ });
87
+ }
73
88
  }
74
89
  }
75
- onWriteFile(fileUrl, stateAfter.buffer, reason);
90
+ if (shouldCompare) {
91
+ onWriteFile(fileUrl, stateAfter.buffer, reason);
92
+ }
93
+ // "ignore", false, anything else
76
94
  };
77
95
  const onWriteDirectoryDone = (directoryUrl) => {
78
- if (!shouldReport(directoryUrl)) {
79
- return;
80
- }
81
- if (undoFilesystemSideEffects && !fileRestoreMap.has(directoryUrl)) {
82
- fileRestoreMap.set(directoryUrl, () => {
83
- removeDirectorySync(directoryUrl, {
84
- allowUseless: true,
85
- recursive: true,
96
+ const action = getAction(directoryUrl);
97
+ const shouldCompare =
98
+ action === "compare" ||
99
+ action === "compare_presence_only" ||
100
+ action === true;
101
+ if (action === "undo" || shouldCompare) {
102
+ if (undoFilesystemSideEffects && !fileRestoreMap.has(directoryUrl)) {
103
+ fileRestoreMap.set(directoryUrl, () => {
104
+ removeDirectorySync(directoryUrl, {
105
+ allowUseless: true,
106
+ recursive: true,
107
+ });
86
108
  });
87
- });
109
+ }
110
+ }
111
+ if (shouldCompare) {
112
+ onWriteDirectory(directoryUrl);
88
113
  }
89
- onWriteDirectory(directoryUrl);
114
+ // "ignore", false, anything else
90
115
  };
91
116
  const restoreCallbackSet = new Set();
92
117
 
@@ -101,7 +126,9 @@ export const spyFilesystemCalls = (
101
126
  _internalFs,
102
127
  "mkdir",
103
128
  (directoryPath, mode, recursive) => {
104
- const directoryUrl = pathToFileURL(directoryPath);
129
+ const directoryUrl = ensurePathnameTrailingSlash(
130
+ pathToFileURL(directoryPath),
131
+ );
105
132
  const stateBefore = getDirectoryState(directoryPath);
106
133
  if (!stateBefore.found && recursive) {
107
134
  const ancestorNotFoundArray = [];
@@ -110,7 +137,9 @@ export const spyFilesystemCalls = (
110
137
  if (ancestorState.found) {
111
138
  break;
112
139
  }
113
- ancestorNotFoundArray.unshift(ancestorUrl);
140
+ ancestorNotFoundArray.unshift(
141
+ ensurePathnameTrailingSlash(ancestorUrl),
142
+ );
114
143
  }
115
144
  return {
116
145
  return: (fd) => {
@@ -151,6 +180,13 @@ export const spyFilesystemCalls = (
151
180
  },
152
181
  { execute: METHOD_EXECUTION_NODE_CALLBACK },
153
182
  );
183
+ /*
184
+ * Relying on open/close to detect writes is done to be able to catch
185
+ * write done async, not sure how to distinguish open/close done to write
186
+ * from open/close done to read file stat
187
+ * open/close for file stat are excluded because we compare stateBefore/stateAfter
188
+ * but ideally we would early return by detecting open/close is not for write operations
189
+ */
154
190
  const closeHook = hookIntoMethod(
155
191
  _internalFs,
156
192
  "close",
@@ -4,7 +4,7 @@ import { groupLogSideEffects } from "./group_log_side_effects.js";
4
4
 
5
5
  const logSideEffectsOptionsDefault = {
6
6
  prevent: true,
7
- group: false,
7
+ group: true,
8
8
  };
9
9
 
10
10
  export const logSideEffects = (logSideEffectsOptions) => {
@@ -37,8 +37,7 @@ export const createBigSizeEffect =
37
37
  export const renderSideEffects = (
38
38
  sideEffects,
39
39
  {
40
- sideEffectFileUrl,
41
- outDirectoryUrl,
40
+ sideEffectMdFileUrl,
42
41
  generateOutFileUrl,
43
42
  generatedBy = true,
44
43
  titleLevel = 1,
@@ -50,6 +49,7 @@ export const renderSideEffects = (
50
49
  dedicatedFile: { line: 50, length: 5000 },
51
50
  }),
52
51
  errorStackHidden,
52
+ errorMessageTransform,
53
53
  } = {},
54
54
  ) => {
55
55
  const { rootDirectoryUrl, replaceFilesystemWellKnownValues } =
@@ -85,14 +85,14 @@ export const renderSideEffects = (
85
85
  markdown += "\n\n";
86
86
  }
87
87
  markdown += renderOneSideEffect(sideEffect, {
88
- sideEffectFileUrl,
89
- outDirectoryUrl,
88
+ sideEffectMdFileUrl,
90
89
  generateOutFileUrl,
91
90
  rootDirectoryUrl,
92
91
  titleLevel,
93
92
  getBigSizeEffect,
94
93
  replace,
95
94
  errorStackHidden,
95
+ errorMessageTransform,
96
96
  lastSideEffectNumber,
97
97
  });
98
98
  }
@@ -133,14 +133,14 @@ ${" ".repeat(indent)}</sub>`;
133
133
  const renderOneSideEffect = (
134
134
  sideEffect,
135
135
  {
136
- sideEffectFileUrl,
137
- outDirectoryUrl,
136
+ sideEffectMdFileUrl,
138
137
  generateOutFileUrl,
139
138
  rootDirectoryUrl,
140
139
  titleLevel,
141
140
  getBigSizeEffect,
142
141
  replace,
143
142
  errorStackHidden,
143
+ errorMessageTransform,
144
144
  lastSideEffectNumber,
145
145
  },
146
146
  ) => {
@@ -152,8 +152,7 @@ const renderOneSideEffect = (
152
152
  }
153
153
  const { md } = sideEffect.render;
154
154
  let { label, text } = md({
155
- sideEffectFileUrl,
156
- outDirectoryUrl,
155
+ sideEffectMdFileUrl,
157
156
  generateOutFileUrl,
158
157
  replace,
159
158
  rootDirectoryUrl,
@@ -172,11 +171,12 @@ const renderOneSideEffect = (
172
171
  }
173
172
  text = renderText(text, {
174
173
  sideEffect,
175
- sideEffectFileUrl,
174
+ sideEffectMdFileUrl,
176
175
  generateOutFileUrl,
177
176
  replace,
178
177
  rootDirectoryUrl,
179
178
  errorStackHidden,
179
+ errorMessageTransform,
180
180
  });
181
181
  }
182
182
  if (sideEffect.code === "source_code") {
@@ -208,11 +208,12 @@ const renderText = (
208
208
  text,
209
209
  {
210
210
  sideEffect,
211
- sideEffectFileUrl,
211
+ sideEffectMdFileUrl,
212
212
  generateOutFileUrl,
213
213
  replace,
214
214
  rootDirectoryUrl,
215
215
  errorStackHidden,
216
+ errorMessageTransform,
216
217
  },
217
218
  ) => {
218
219
  if (text && typeof text === "object") {
@@ -224,7 +225,7 @@ const renderText = (
224
225
  }
225
226
  const callSiteRelativeUrl = urlToRelativeUrl(
226
227
  callSite.url,
227
- sideEffectFileUrl,
228
+ sideEffectMdFileUrl,
228
229
  { preferRelativeNotation: true },
229
230
  );
230
231
  const sourceCodeLinkText = `${callSiteRelativeUrl}:${callSite.line}:${callSite.column}`;
@@ -250,14 +251,17 @@ const renderText = (
250
251
  typeof jsValue.stack === "string")
251
252
  ) {
252
253
  // return renderMarkdownBlock(text.value.stack);
253
- const exception = createException(jsValue, { rootDirectoryUrl });
254
+ const exception = createException(jsValue, {
255
+ rootDirectoryUrl,
256
+ errorMessageTransform,
257
+ });
254
258
  const exceptionText = errorStackHidden
255
259
  ? `${exception.name}: ${exception.message}`
256
260
  : exception.stack || exception.message || exception;
257
261
  return renderPotentialAnsi(exceptionText, {
258
262
  stringType: "error",
259
263
  sideEffect,
260
- sideEffectFileUrl,
264
+ sideEffectMdFileUrl,
261
265
  generateOutFileUrl,
262
266
  replace,
263
267
  });
@@ -267,7 +271,7 @@ const renderText = (
267
271
  if (text.type === "console") {
268
272
  return renderConsole(text.value, {
269
273
  sideEffect,
270
- sideEffectFileUrl,
274
+ sideEffectMdFileUrl,
271
275
  generateOutFileUrl,
272
276
  replace,
273
277
  });
@@ -287,12 +291,12 @@ const renderText = (
287
291
 
288
292
  export const renderConsole = (
289
293
  string,
290
- { sideEffect, sideEffectFileUrl, generateOutFileUrl, replace },
294
+ { sideEffect, sideEffectMdFileUrl, generateOutFileUrl, replace },
291
295
  ) => {
292
296
  return renderPotentialAnsi(string, {
293
297
  stringType: "console",
294
298
  sideEffect,
295
- sideEffectFileUrl,
299
+ sideEffectMdFileUrl,
296
300
  generateOutFileUrl,
297
301
  replace,
298
302
  });
@@ -300,7 +304,7 @@ export const renderConsole = (
300
304
 
301
305
  const renderPotentialAnsi = (
302
306
  string,
303
- { stringType, sideEffect, sideEffectFileUrl, generateOutFileUrl, replace },
307
+ { stringType, sideEffect, sideEffectMdFileUrl, generateOutFileUrl, replace },
304
308
  ) => {
305
309
  const rawTextBlock = renderMarkdownBlock(
306
310
  replace(string, { stringType }),
@@ -324,7 +328,7 @@ const renderPotentialAnsi = (
324
328
  );
325
329
  svgFileContent = replace(svgFileContent, { fileUrl: svgFileUrl });
326
330
  writeFileSync(svgFileUrl, svgFileContent);
327
- const svgFileRelativeUrl = urlToRelativeUrl(svgFileUrl, sideEffectFileUrl);
331
+ const svgFileRelativeUrl = urlToRelativeUrl(svgFileUrl, sideEffectMdFileUrl);
328
332
  let md = `![img](${svgFileRelativeUrl})`;
329
333
  md += "\n\n";
330
334
  md += renderMarkdownDetails(`${rawTextBlock}`, {
@@ -1,69 +1,71 @@
1
- import { urlToBasename } from "@jsenv/urls";
2
- import {
3
- takeDirectorySnapshot,
4
- takeFileSnapshot,
5
- } from "../filesystem_snapshot.js";
1
+ import { writeFileSync } from "@jsenv/filesystem";
2
+ import { urlToBasename, urlToFilename } from "@jsenv/urls";
3
+ import { takeDirectorySnapshot } from "../filesystem_snapshot.js";
6
4
  import { createCaptureSideEffects } from "./create_capture_side_effects.js";
7
5
  import { renderSideEffects } from "./render_side_effects.js";
8
-
6
+ /**
7
+ * Generate a markdown file describing code side effects. When executed in CI throw if there is a diff.
8
+ * @param {URL} sourceFileUrl
9
+ * Url where the function is located (import.meta.url)
10
+ * @param {Function} fn
11
+ * Function to snapshot
12
+ * @param {Object} snapshotSideEffectsOptions
13
+ * @param {string|url} snapshotSideEffectsOptions.outFilePattern
14
+ * @param {string|url} snapshotSideEffectsOptions.sideEffectMdFileUrl
15
+ * Where to write the markdown file. Defaults to ./[]
16
+ * @param {string|url} snapshotSideEffectsOptions.rootDirectoryUrl
17
+ * @param {Object|boolean} [snapshotSideEffectsOptions.filesystemEffects]
18
+ * @param {boolean} [snapshotSideEffectsOptions.filesystemEffects.textualFilesInline=false]
19
+ * Put textual files content in the markdown (instead of separate files).
20
+ * Big files will still be put in dedicated files.
21
+ * @param {boolean} [snapshotSideEffectsOptions.filesystemEffects.preserve=false]
22
+ * Preserve filesystem side effect when function ends. By default
23
+ * filesystem effects are undone when function ends
24
+ * @param {url} [snapshotSideEffectsOptions.filesystemEffects.baseDirectory]
25
+ * Urls of filesystem side effects will be relative to this base directory
26
+ * Default to the directory containing @sourceFileUrl
27
+ * @return {Array.<Object>} sideEffects
28
+ */
9
29
  export const snapshotSideEffects = (
10
30
  sourceFileUrl,
11
31
  fn,
12
32
  {
13
- sideEffectFileUrl,
14
- outDirectoryPattern = "./side_effects/",
15
- sideEffectFilePattern = "./side_effects/[basename].md",
16
- outFilePattern = "./side_effects/[name]/[filename]",
17
- generateOutFileUrl,
18
- outDirectoryUrl,
33
+ sideEffectMdFileUrl,
34
+ outFilePattern = "_[source_filename]/[filename]",
19
35
  errorStackHidden,
36
+ throwWhenDiff,
20
37
  ...captureOptions
21
38
  } = {},
22
39
  ) => {
23
- const name = urlToBasename(sourceFileUrl, true);
24
- const basename = urlToBasename(sourceFileUrl);
25
- if (sideEffectFileUrl === undefined) {
26
- const sideEffectFileRelativeUrl = sideEffectFilePattern
27
- .replaceAll("[basename]", basename)
28
- .replaceAll("[name]", name);
29
- sideEffectFileUrl = new URL(sideEffectFileRelativeUrl, sourceFileUrl);
30
- } else {
31
- sideEffectFileUrl = new URL(sideEffectFileUrl, sourceFileUrl);
32
- }
33
-
34
- const captureSideEffects = createCaptureSideEffects(captureOptions);
35
- if (outDirectoryUrl === undefined) {
36
- const outDirectoryRelativeUrl = outDirectoryPattern.replaceAll(
37
- "[basename]",
38
- basename,
39
- );
40
- outDirectoryUrl = new URL(outDirectoryRelativeUrl, sideEffectFileUrl);
41
- }
42
- if (generateOutFileUrl === undefined) {
43
- generateOutFileUrl = (filename) => {
44
- const outRelativeUrl = outFilePattern
45
- .replaceAll("[name]", name)
46
- .replaceAll("[basename]", basename)
47
- .replaceAll("[filename]", filename);
48
- const outFileUrl = new URL(outRelativeUrl, new URL("./", sourceFileUrl))
49
- .href;
50
- return outFileUrl;
51
- };
52
- }
53
-
54
- const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
40
+ const sourceName = urlToBasename(sourceFileUrl, true);
41
+ const sourceBasename = urlToBasename(sourceFileUrl);
42
+ const sourceFilename = urlToFilename(sourceFileUrl);
43
+ const generateOutFileUrl = (filename) => {
44
+ const outRelativeUrl = outFilePattern
45
+ .replaceAll("[source_name]", sourceName)
46
+ .replaceAll("[source_basename]", sourceBasename)
47
+ .replaceAll("[source_filename]", sourceFilename)
48
+ .replaceAll("[filename]", filename);
49
+ const outFileUrl = new URL(outRelativeUrl, new URL("./", sourceFileUrl))
50
+ .href;
51
+ return outFileUrl;
52
+ };
53
+ const outDirectoryUrl = generateOutFileUrl("");
54
+ sideEffectMdFileUrl =
55
+ sideEffectMdFileUrl || generateOutFileUrl(`${sourceFilename}.md`);
56
+ const captureSideEffects = createCaptureSideEffects({
57
+ ...captureOptions,
58
+ sourceFileUrl,
59
+ });
55
60
  const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl);
56
61
  const onSideEffects = (sideEffects) => {
57
62
  const sideEffectFileContent = renderSideEffects(sideEffects, {
58
- sideEffectFileUrl,
59
- outDirectoryUrl,
63
+ sideEffectMdFileUrl,
60
64
  generateOutFileUrl,
61
65
  errorStackHidden,
62
66
  });
63
- sideEffectFileSnapshot.update(sideEffectFileContent, {
64
- mockFluctuatingValues: false,
65
- });
66
- outDirectorySnapshot.compare();
67
+ writeFileSync(sideEffectMdFileUrl, sideEffectFileContent);
68
+ outDirectorySnapshot.compare(throwWhenDiff);
67
69
  };
68
70
  const returnValue = captureSideEffects(fn);
69
71
  if (returnValue && typeof returnValue.then === "function") {
@@ -1,49 +1,66 @@
1
- import { urlToBasename, urlToRelativeUrl } from "@jsenv/urls";
2
- import {
3
- takeDirectorySnapshot,
4
- takeFileSnapshot,
5
- } from "../filesystem_snapshot.js";
1
+ import { writeFileSync } from "@jsenv/filesystem";
2
+ import { urlToBasename, urlToFilename, urlToRelativeUrl } from "@jsenv/urls";
3
+ import { takeDirectorySnapshot } from "../filesystem_snapshot.js";
6
4
  import { getCallerLocation } from "../get_caller_location.js";
7
5
  import { createCaptureSideEffects } from "./create_capture_side_effects.js";
8
6
  import { renderSideEffects, renderSmallLink } from "./render_side_effects.js";
9
7
 
10
8
  /**
11
- * Generate a markdown file describing all test side effects. When executed in CI throw if there is a diff.
12
- * @param {URL} testFileUrl
9
+ * Generate a markdown file describing test(s) side effects. When executed in CI throw if there is a diff.
10
+ * @param {URL} sourceFileUrl
13
11
  * @param {Function} fnRegisteringTest
14
12
  * @param {Object} snapshotTestsOptions
15
- * @param {string|url} snapshotTestsOptions.sideEffectFileUrl
13
+ * @param {string|url} snapshotTestsOptions.outFilePattern
16
14
  * @param {string|url} snapshotTestsOptions.rootDirectoryUrl
17
- * @return {Array.<Object>} sideEffects
15
+ * @param {Object|boolean} [snapshotTestsOptions.filesystemEffects]
16
+ * @param {boolean} [snapshotTestsOptions.filesystemEffects.textualFilesInline=false]
17
+ * Put textual files content in the markdown (instead of separate files).
18
+ * Big files will still be put in dedicated files.
19
+ * @param {boolean} [snapshotTestsOptions.filesystemEffects.preserve=false]
20
+ * Preserve filesystem side effect when function ends. By default
21
+ * filesystem effects are undone when function ends
22
+ * @param {url} [snapshotTestsOptions.filesystemEffects.baseDirectory]
23
+ * Urls of filesystem side effects will be relative to this base directory
24
+ * Default to the directory containing @sourceFileUrl
18
25
  */
19
26
  export const snapshotTests = async (
20
- testFileUrl,
27
+ sourceFileUrl,
21
28
  fnRegisteringTest,
22
29
  {
23
- testName = urlToBasename(testFileUrl, true),
24
- sideEffectFileUrl,
25
- outDirectoryPattern = "./side_effects/",
26
- sideEffectFilePattern = "./side_effects/[test_basename].md",
27
- outFilePattern = "./side_effects/[test_name]/[test_scenario]/[filename]",
30
+ outFilePattern = "./_[source_filename]/[filename]",
31
+ filesystemActions = {
32
+ "**": "compare",
33
+ // "**/*.svg": "compare_presence_only",
34
+ },
28
35
  rootDirectoryUrl,
29
36
  generatedBy = true,
30
37
  linkToSource = true,
31
38
  linkToEachSource,
32
39
  errorStackHidden,
40
+ errorMessageTransform,
33
41
  logEffects,
34
42
  filesystemEffects,
35
43
  throwWhenDiff = process.env.CI,
36
44
  } = {},
37
45
  ) => {
38
- const testBasename = urlToBasename(testFileUrl);
39
- if (sideEffectFileUrl === undefined) {
40
- const sideEffectFileRelativeUrl = sideEffectFilePattern
41
- .replaceAll("[test_name]", testName)
42
- .replaceAll("[test_basename]", testBasename);
43
- sideEffectFileUrl = new URL(sideEffectFileRelativeUrl, testFileUrl);
44
- } else {
45
- sideEffectFileUrl = new URL(sideEffectFileUrl, testFileUrl);
46
- }
46
+ const sourceName = urlToBasename(sourceFileUrl, true);
47
+ const sourceBasename = urlToBasename(sourceFileUrl);
48
+ const sourceFilename = urlToFilename(sourceFileUrl);
49
+ const generateOutFileUrl = (outFilename) => {
50
+ const outFileRelativeUrl = outFilePattern
51
+ .replaceAll("[source_name]", sourceName)
52
+ .replaceAll("[source_basename]", sourceBasename)
53
+ .replaceAll("[source_filename]", sourceFilename)
54
+ .replaceAll("[filename]", outFilename);
55
+ const outFileUrl = new URL(outFileRelativeUrl, sourceFileUrl).href;
56
+ return outFileUrl;
57
+ };
58
+ const outDirectoryUrl = generateOutFileUrl("");
59
+ const outDirectorySnapshot = takeDirectorySnapshot(
60
+ outDirectoryUrl,
61
+ filesystemActions,
62
+ );
63
+ const sideEffectMdFileUrl = generateOutFileUrl(`${sourceFilename}.md`);
47
64
 
48
65
  const dirUrlMap = new Map();
49
66
  const sideEffectsMap = new Map();
@@ -65,12 +82,14 @@ export const snapshotTests = async (
65
82
 
66
83
  const activeTestMap = onlyTestMap.size ? onlyTestMap : testMap;
67
84
  const captureSideEffects = createCaptureSideEffects({
85
+ sourceFileUrl,
68
86
  rootDirectoryUrl,
69
87
  logEffects,
70
88
  filesystemEffects,
89
+ filesystemActions,
71
90
  });
72
91
  let markdown = "";
73
- markdown += `# ${testName}`;
92
+ markdown += `# ${sourceName}`;
74
93
  if (generatedBy) {
75
94
  let generatedByLink = renderSmallLink(
76
95
  {
@@ -80,8 +99,8 @@ export const snapshotTests = async (
80
99
  {
81
100
  prefix: "Generated by ",
82
101
  suffix:
83
- linkToSource && testFileUrl
84
- ? generateExecutingLink(testFileUrl, sideEffectFileUrl)
102
+ linkToSource && sourceFileUrl
103
+ ? generateExecutingLink(sourceFileUrl, sideEffectMdFileUrl)
85
104
  : "",
86
105
  },
87
106
  );
@@ -89,6 +108,7 @@ export const snapshotTests = async (
89
108
  markdown += generatedByLink;
90
109
  }
91
110
 
111
+ const scenarioDirs = [];
92
112
  for (const [scenario, { fn, callSite }] of activeTestMap) {
93
113
  markdown += "\n\n";
94
114
  markdown += `## ${scenario}`;
@@ -99,43 +119,40 @@ export const snapshotTests = async (
99
119
  });
100
120
  sideEffectsMap.set(scenario, sideEffects);
101
121
  const testScenario = asValidFilename(scenario);
102
- let outDirectoryRelativeUrl = outDirectoryPattern
103
- .replaceAll("[test_name]", testName)
104
- .replaceAll("[test_scenario]", testScenario);
105
- const outDirectoryUrl = new URL(outDirectoryRelativeUrl, testFileUrl);
106
- const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl, {
107
- pattern: {
108
- "**/*": true,
109
- "**/*.svg": "presence_only",
110
- },
111
- });
112
- const generateOutFileUrl = (filename) => {
113
- const outFileRelativeUrl = outFilePattern
114
- .replaceAll("[test_name]", testName)
115
- .replaceAll("[test_basename]", testBasename)
116
- .replaceAll("[test_scenario]", testScenario)
117
- .replaceAll("[filename]", filename);
118
- const outFileUrl = new URL(outFileRelativeUrl, testFileUrl).href;
119
- return outFileUrl;
122
+ scenarioDirs.push(testScenario);
123
+ const generateScenarioOutFileUrl = (outfilename) => {
124
+ return generateOutFileUrl(`${testScenario}/${outfilename}`);
120
125
  };
121
- const outFileDirectoryUrl = generateOutFileUrl("");
122
- dirUrlMap.set(scenario, outFileDirectoryUrl);
126
+ const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
127
+ dirUrlMap.set(scenario, scenarioOutDirectoryUrl);
123
128
  const sideEffectsMarkdown = renderSideEffects(sideEffects, {
124
- sideEffectFileUrl,
125
- outDirectoryUrl,
126
- generateOutFileUrl,
129
+ sideEffectMdFileUrl,
130
+ generateOutFileUrl: generateScenarioOutFileUrl,
127
131
  generatedBy: false,
128
132
  titleLevel: 3,
129
133
  errorStackHidden,
134
+ errorMessageTransform,
130
135
  });
131
- outDirectorySnapshot.compare(throwWhenDiff);
132
136
  markdown += sideEffectsMarkdown;
133
137
  }
134
- const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
135
- sideEffectFileSnapshot.update(markdown, {
136
- mockFluctuatingValues: false,
137
- throwWhenDiff,
138
- });
138
+ // if (sideEffectFilePattern === "./side_effects/[filename]/[filename].md") {
139
+ // const scenarioParentDirUrl = new URL("./", sideEffectFileUrl);
140
+ // const dirContent = readDirectorySync(scenarioParentDirUrl);
141
+ // for (const entry of dirContent) {
142
+ // const entryUrl = new URL(entry, scenarioParentDirUrl);
143
+ // if (!readEntryStatSync(entryUrl).isDirectory()) {
144
+ // continue;
145
+ // }
146
+ // if (scenarioDirs.includes(entry)) {
147
+ // continue;
148
+ // }
149
+ // removeDirectorySync(entryUrl, {
150
+ // recursive: true,
151
+ // });
152
+ // }
153
+ // }
154
+ writeFileSync(sideEffectMdFileUrl, markdown);
155
+ outDirectorySnapshot.compare(throwWhenDiff);
139
156
 
140
157
  return { dirUrlMap, sideEffectsMap };
141
158
  };